package gov.fnal.xpath2ldap;

import org.w3c.dom.Document;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Stack;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Iterator;
import java.util.Map;
import java.util.Arrays;

/**
 * 
 * User: tdh
 * Date: Feb 23, 2009
 * Time: 11:27:21 AM
 *
 */


public class LPathSearch {

    XMLAdapter xmler;

    public LPathSearch(XMLAdapter xmler) {
        this.xmler = xmler;
    }

    public Pair<Document, LinkedHashMap<String, Stack<String>>> search(List<String> lFullPaths) {
        return search(lFullPaths, null);
    }
    public Pair<Document, LinkedHashMap<String, Stack<String>>> search(List<String> lFullPaths, Document xmlforattr) {

        LinkedHashSet<String> allSearches = new LinkedHashSet<String>();
        allSearches.addAll(lFullPaths);

        // Compose the filters and search the ldap directory
        LinkedHashMap<String, LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>>> searchSet =
                new LinkedHashMap<String, LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>>>();
        LinkedHashMap<String, Stack<String>>ancestorMap = new LinkedHashMap<String, Stack<String>>();
        for (String searchstr : allSearches) {
            String[] eltnames=null;
            LinkedHashSet<String> search_filters = new LinkedHashSet<String>();
            LinkedHashSet<String> search_attributes = new LinkedHashSet<String>();
            int i=0;
            System.out.println("SEARCH " + searchstr);
            // //a/@b=1 and //c/@d=2 and e/f/@g=3
            String[] uncombined = searchstr.split(" ");
            String operator = null;
            if("and".equals(uncombined[0])) { //todo: add check that all are "and" or all are "or"
                operator = "&";
            } else if ("or".equals(uncombined[0])) {
                operator = "|";
            }
            if(operator!=null) {
                for (int j=1; j<uncombined.length; j++) {
                    // Combine attributes for use with ldap operator symbol
                    String subsearchstr = uncombined[j];
                    eltnames = subsearchstr.split("/");
                    i = eltnames.length-1;
                    String lastf = eltnames[i];
                    if(lastf.startsWith("@")) {
                        lastf = lastf.substring(1, lastf.length());
                        if (lastf.indexOf('>')!=-1) {
                            search_filters.add(lastf);
                            search_attributes.add(lastf.substring(0, lastf.indexOf('>')));
                        } else if (lastf.indexOf('<')!=-1) {
                            search_filters.add(lastf);
                            search_attributes.add(lastf.substring(0, lastf.indexOf('<')));
                        } else if (lastf.indexOf('!')!=-1) {
                            search_filters.add(lastf);
                            search_attributes.add(lastf.substring(0, lastf.indexOf('!')));
                        } else if(lastf.indexOf('=')!=-1) {
                            search_filters.add(lastf);
                            search_attributes.add(lastf.substring(0, lastf.indexOf('=')));
                        } else {
                            search_attributes.add(lastf);
                        }
                    } else {
                        search_attributes.add("objectclass");
                        i = eltnames.length;
                    }
                }
            } else {
                // Pick out attribute to search for
                eltnames = searchstr.split("/");
                i = eltnames.length-1;
                String lastf = eltnames[i];
                if(lastf.startsWith("@")) {
                    lastf = lastf.substring(1, lastf.length());
                    if (lastf.indexOf('>')!=-1) {
                        search_filters.add(lastf);
                        search_attributes.add(lastf.substring(0, lastf.indexOf('>')));
                    } else if (lastf.indexOf('<')!=-1) {
                        search_filters.add(lastf);
                        search_attributes.add(lastf.substring(0, lastf.indexOf('<')));
                    } else if (lastf.indexOf('!')!=-1) {
                        search_filters.add(lastf);
                        search_attributes.add(lastf.substring(0, lastf.indexOf('!')));
                    } else if(lastf.indexOf('=')!=-1) {
                        search_filters.add(lastf);
                        search_attributes.add(lastf.substring(0, lastf.indexOf('=')));
                    } else {
                        search_attributes.add(lastf);
                    }
                } else {
                    search_attributes.add("objectclass");
                    i = eltnames.length;
                }
            }
            // Find first common ancestor of all search objects. //todo: only create unique ancestors to that point, then add to common ancestor
            // The i index now points to just after the last element
            boolean set_filter=false;
            boolean skip_next_ancestor=false;
            Stack<String>ancestorSet = new Stack<String>();
            while(i-->0)  {
                String classname = eltnames[i];
                boolean is_auxiliary = xmler.isAuxiliaryObjectclass(classname);
                boolean is_structural = xmler.isStructuralObjectclass(classname);
                if(!is_auxiliary && !is_structural) { // todo get rid of spurious "(" as first element
                    continue;
                }
                // If the filter has already been set, just add the objectclass to the ancestors (if it is structural)
                if(set_filter) {
                    if(is_structural) {
                        // If the filter was set from an auxiliary class, skip adding the first ancestor
                        if(!skip_next_ancestor) {
                            ancestorSet.add(classname);
                        } else {
                            // Set the ancestor map from the first structural objectclass
                            ancestorMap.put(classname, ancestorSet);
                            skip_next_ancestor=false;
                        }
                    }
                    continue;
                }
                String filter=null;
                if(search_filters.size()==1) {
                    filter = "(" + search_filters.iterator().next().replaceAll("'", "") + ")";
                } else if(search_filters.size()>1) {
                    filter = "(" + operator;
                    for(String attrfilter : search_filters) {
                        filter += "(" + attrfilter.replaceAll("'", "") + ")";
                    }
                    filter += ")";
                }
                search_filters=new LinkedHashSet<String>();
                search_filters.add(filter);
                search_attributes.add("objectclass");
                if(searchSet.containsKey(classname)) {
                    LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>> params = searchSet.get(classname);
                    Iterator<Pair<LinkedHashSet<String>, LinkedHashSet<String>>> it = params.iterator();
                    Pair<LinkedHashSet<String>,LinkedHashSet<String>> mayadd =
                            new Pair<LinkedHashSet<String>,LinkedHashSet<String>>(search_filters, search_attributes);
                    int mayhashcode = mayadd.hashCode();
                    boolean does_contain=false;
                    while (it.hasNext()) {
                        Object obj = it.next();
                        if (obj != null) {
                            int hashashcode = obj.hashCode();
                            if(hashashcode==mayhashcode) {
                                does_contain=true;
                                break;
                            }
                        }
                    }
                    if(!does_contain) {
                        params.add(mayadd);
                    }
                } else {
                    LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>> params =
                            new LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>>();
                    params.add(new Pair<LinkedHashSet<String>,LinkedHashSet<String>>(search_filters, search_attributes));
                    searchSet.put(classname, params);
                }
                set_filter=true;
                // If the filter was set from an auxiliary element,
                // its dsml entry will have its first structual ancestor
                // as its objectclass. So we want the ancestors to start
                // before that object class.
                if(is_auxiliary) {  // todo: transforms are currently putting the attribute under both the auxiliary and parent structural nodes
                    skip_next_ancestor=true;
                } else {
                    // Set the ancestor map from the first structural objectclass
                    Stack<String> savedAncestorSet = ancestorMap.get(classname);
                    if(savedAncestorSet==null || ancestorSet.size()>savedAncestorSet.size()) {
                        ancestorMap.put(classname, ancestorSet);
                    }
                }
            }
        }

        xmlforattr = search(searchSet, xmlforattr);

        return new Pair<Document, LinkedHashMap<String, Stack<String>>>(xmlforattr, ancestorMap);
    }

    public Document search(LinkedHashMap<String, LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>>> searchSet) {
        return search(searchSet, null);
    }

    public Document search(LinkedHashMap<String, LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>>> searchSet, Document xmlforattr) {
                // Perform a search for one the attributes
        String s_filter;
        String[] s_attributes;
        // Collects results of all searches

        for(Map.Entry<String, LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>>> entry : searchSet.entrySet()) {
            String classname = entry.getKey();
            LinkedHashSet<Pair<LinkedHashSet<String>,LinkedHashSet<String>>> params = entry.getValue();
            for(Pair<LinkedHashSet<String>,LinkedHashSet<String>> pair : params) {  //todo: presently uses only result from last pair
                LinkedHashSet<String> search_filters = pair.getFirst();
                String filter = "";
                for(String lfilt : search_filters) {
                    if(lfilt!=null) {
                        filter += lfilt;
                    }
                }
                s_filter = ("".equals(filter)) ? "objectclass=" + classname : "(&(objectclass=" + classname + ")" + filter + ")";
                LinkedHashSet<String> search_attributes = pair.getSecond();
                s_attributes = search_attributes.toArray(new String[]{});
                System.out.print(s_filter + " "); for (String a : s_attributes) { System.out.print(a + " ");} System.out.print("\n");
                xmlforattr = xmler.constructXML(s_filter, s_attributes, xmlforattr);
                //resultSet.add(xmlforattr); //todo: merge results for one objectclass
            }
        }

            return xmlforattr;
    }


    public void combineCommonAncestors(String ldap_expression) {
        Pattern enclosingparens = Pattern.compile("\\(([^()]*\\([^()]*\\))+[^()]*\\)");
        Pattern innerparens = Pattern.compile("([^()]*)(\\([^()]*\\))");
        Matcher matcher;
        matcher = enclosingparens.matcher(ldap_expression);
        LinkedHashSet<String> allSearches = new LinkedHashSet<String>();
        // Find the innermost parens that are enclosed in parens
        while(matcher.find()) {
            String enclosing = matcher.group(0);
            // Find the inner parentheses of this match
            Matcher matcher2 = innerparens.matcher(enclosing);
            LinkedHashSet<String> operators = new LinkedHashSet<String>();
            LinkedHashSet<String> searches = new LinkedHashSet<String>();
            LinkedHashMap<String, LinkedHashSet<String>> ancestors = new LinkedHashMap<String, LinkedHashSet<String>>();
            while(matcher2.find()) {
                String op = matcher2.group(1);
                if(!"".equals(op)) {
                    operators.add(op);
                }
                String attr = matcher2.group(2);
                if(!"".equals(attr)) {
                    int ampindex = attr.indexOf("@");
                    if(ampindex>=0) {
                    String ancestor = attr.substring(0, ampindex);
                    if(ancestors.containsKey(ancestor)) {
                        ancestors.get(ancestor).add(attr);
                    } else {
                        ancestors.put(ancestor, new LinkedHashSet<String>(Arrays.asList(attr)));
                    }
                    searches.add(attr);
                    }
                }
            }

            // Combine these in one ldap search if all operators are the same and some attributes have common ancestors
            if(operators.size()<=1 && ancestors.size()<searches.size()) {
                searches = new LinkedHashSet<String>();
                Iterator<Map.Entry<String,LinkedHashSet<String>>> it = ancestors.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry<String,LinkedHashSet<String>> pair = it.next();
                    LinkedHashSet<String> siblattrs = pair.getValue();
                    if(siblattrs.size()==1) {
                        searches.add(siblattrs.iterator().next());
                    } else {
                        String combined = (String) operators.iterator().next().trim();
                        for (String sibattr : siblattrs) {
                            combined +=  " " + sibattr;
                        }
                        searches.add(combined);
                    }
                }
            }
            allSearches.addAll(searches);
            // Remove the match from the original string
            Pattern p = Pattern.compile(Pattern.quote(enclosing));
            Matcher m = p.matcher(ldap_expression); // get a matcher object
            ldap_expression = m.replaceAll("");
        }
        // Look for parentheses in remaining string todo: rewrite to order from innermost to outermost, or perform actual stack processing
        // in order to find the rest of the attributes
        Matcher matcher3 = innerparens.matcher(ldap_expression);
        while(matcher3.find()) {
            String attr = matcher3.group(2);
            if(!"".equals(attr)) {
                allSearches.add(attr);
            }
        }
    }
}
